Batch Approval for AI Tool Calls, fix "AI is thinking" message, chunk JS#2430
Batch Approval for AI Tool Calls, fix "AI is thinking" message, chunk JS#2430
Conversation
Walkthrough
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
pkg/aiusechat/usechat.go (1)
313-319: Persist initial ToolUseData to chat store as wellPre-emitting SSE is great. Consider also persisting the initial ToolUseData so a reconnect/history load reflects it without relying on prior SSE.
Apply this diff to also update the chat:
for _, toolCall := range stopReason.ToolCalls { if toolCall.ToolUseData != nil { log.Printf("AI data-tooluse %s\n", toolCall.ID) _ = sseHandler.AiMsgData("data-tooluse", toolCall.ID, *toolCall.ToolUseData) + updateToolUseDataInChat(chatOpts, toolCall.ID, toolCall.ToolUseData) } }frontend/app/aipanel/aimessage.tsx (2)
121-127: Group title can be wrong when mixing file reads and directory listingsA single batch may contain both read_text_file and read_dir; using the first item to pick title can mislabel the group.
Split batches by toolname (reads vs listings) or compute a combined label, e.g., “Reading files and listing directories”.
128-149: Prefer stable keys (toolcallid) over array indicesIndex keys cause unnecessary re-renders and can break UI state when arrays change.
Apply this diff:
- {parts.map((part, idx) => { + {parts.map((part) => { @@ - <div key={idx} className="text-sm pl-2"> + <div key={part.data.toolcallid} className="text-sm pl-2">- {otherTools.map((tool, idx) => ( - <div key={idx} className="mt-2"> + {otherTools.map((tool) => ( + <div key={tool.data.toolcallid} className="mt-2">Also applies to: 290-293
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
.gitignore(1 hunks)electron.vite.config.ts(1 hunks)frontend/app/aipanel/aimessage.tsx(6 hunks)frontend/app/aipanel/aipanel.tsx(1 hunks)pkg/aiusechat/openai/openai-backend.go(0 hunks)pkg/aiusechat/usechat.go(1 hunks)
💤 Files with no reviewable changes (1)
- pkg/aiusechat/openai/openai-backend.go
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aipanel.tsx (1)
frontend/app/aipanel/aipanelmessages.tsx (1)
AIPanelMessages(16-64)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
WaveUIMessagePart(24-24)frontend/app/store/wshclientapi.ts (1)
RpcApi(537-537)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx
[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build for TestDriver.ai
- GitHub Check: Analyze (go)
🔇 Additional comments (3)
.gitignore (1)
20-20: Adding plainoutignore looks fineCovers both
out/dir and anoutfile. Just confirm no legitimate top-level file namedoutneeds to be tracked.frontend/app/aipanel/aipanel.tsx (1)
532-536: Prop reflow only — no functional changeLGTM. Keeps props explicit and readable; no behavior impact.
frontend/app/aipanel/aimessage.tsx (1)
13-21: AIThinking prop is a nice improvementParametric message with a safe guard is clean. LGTM.
| output: { | ||
| manualChunks(id) { | ||
| if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco"; | ||
| if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid")) | ||
| return "mermaid"; | ||
| if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex"; | ||
| if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) { | ||
| return "shiki"; | ||
| } | ||
| if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape")) | ||
| return "cytoscape"; | ||
| return undefined; | ||
| }, | ||
| }, |
There was a problem hiding this comment.
manualChunks path checks may miss packages and Windows paths
- Windows backslashes can break
id.includes("node_modules/..."). - Likely scopes are different:
@shikijs(not@shiki),@monaco-editor(not@monaco),@mermaid-js(not@mermaid).
Use normalized paths and correct scopes:
- output: {
- manualChunks(id) {
- if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
- if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
- return "mermaid";
- if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
- if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
- return "shiki";
- }
- if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
- return "cytoscape";
- return undefined;
- },
- },
+ output: {
+ manualChunks(id) {
+ const p = id.replace(/\\/g, "/");
+ if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor"))
+ return "monaco";
+ if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js"))
+ return "mermaid";
+ if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex")) return "katex";
+ if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs")) return "shiki";
+ if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape"))
+ return "cytoscape";
+ return undefined;
+ },
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| output: { | |
| manualChunks(id) { | |
| if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco"; | |
| if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid")) | |
| return "mermaid"; | |
| if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex"; | |
| if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) { | |
| return "shiki"; | |
| } | |
| if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape")) | |
| return "cytoscape"; | |
| return undefined; | |
| }, | |
| }, | |
| output: { | |
| manualChunks(id) { | |
| // normalize Windows backslashes to forward slashes | |
| const p = id.replace(/\\/g, "/"); | |
| if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor")) | |
| return "monaco"; | |
| if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js")) | |
| return "mermaid"; | |
| if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex")) | |
| return "katex"; | |
| if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs")) | |
| return "shiki"; | |
| if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape")) | |
| return "cytoscape"; | |
| return undefined; | |
| }, | |
| }, |
frontend/app/aipanel/aimessage.tsx
Outdated
| if (parts.length === 0) return null; | ||
|
|
||
| const firstTool = parts[0].data; | ||
| const baseApproval = userApprovalOverride || firstTool.approval; | ||
| const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; | ||
| const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); | ||
|
|
||
| useEffect(() => { | ||
| if (!isStreaming || effectiveApproval !== "needs-approval") return; | ||
|
|
||
| const interval = setInterval(() => { | ||
| parts.forEach((part) => { | ||
| RpcApi.WaveAIToolApproveCommand(TabRpcClient, { | ||
| toolcallid: part.data.toolcallid, | ||
| keepalive: true, | ||
| }); | ||
| }); | ||
| }, 4000); | ||
|
|
||
| return () => clearInterval(interval); | ||
| }, [isStreaming, effectiveApproval, parts]); | ||
|
|
There was a problem hiding this comment.
Fix hook-order violation in AIToolUseBatch (early return before useEffect)
The early return precedes useEffect, violating hooks rule (matches static analysis hint). Always call hooks in the same order.
Apply this diff:
- if (parts.length === 0) return null;
+ const hasParts = parts.length > 0;
@@
- useEffect(() => {
+ useEffect(() => {
if (!isStreaming || effectiveApproval !== "needs-approval") return;
@@
- }, [isStreaming, effectiveApproval, parts]);
+ }, [isStreaming, effectiveApproval, parts]);
+
+ if (!hasParts) return null;Based on static analysis hints
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Biome (2.1.2)
[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🤖 Prompt for AI Agents
frontend/app/aipanel/aimessage.tsx around lines 79 to 100: the component
currently returns early when parts.length === 0 which prevents hooks from being
called in a consistent order; remove the early return and instead compute a
boolean like isEmpty = parts.length === 0 so that useEffect and other hooks are
always invoked unconditionally, keep the same useEffect logic and dependencies,
and finally render null (return null) at the JSX/return point when isEmpty is
true.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
frontend/app/aipanel/aimessage.tsx (1)
121-123: Batch title can be inaccurate for mixed file opsIf the batch contains both read_text_file and read_dir, the title uses only the first item.
Replace with:
- const groupTitle = firstTool.toolname === "read_text_file" ? "Reading Files" : "Listing Directories"; + const hasReadFile = parts.some((p) => p.data.toolname === "read_text_file"); + const hasReadDir = parts.some((p) => p.data.toolname === "read_dir"); + const groupTitle = hasReadFile && hasReadDir ? "File Access" : hasReadFile ? "Reading Files" : "Listing Directories";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
.gitignore(1 hunks)electron.vite.config.ts(1 hunks)frontend/app/aipanel/aimessage.tsx(6 hunks)frontend/app/aipanel/aipanel.tsx(1 hunks)pkg/aiusechat/openai/openai-backend.go(0 hunks)pkg/aiusechat/usechat.go(1 hunks)
💤 Files with no reviewable changes (1)
- pkg/aiusechat/openai/openai-backend.go
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
WaveUIMessagePart(24-24)frontend/app/store/wshclientapi.ts (1)
RpcApi(537-537)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)
frontend/app/aipanel/aipanel.tsx (1)
frontend/app/aipanel/aipanelmessages.tsx (1)
AIPanelMessages(16-64)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx
[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (4)
.gitignore (1)
20-20: No issues found—good safeguard.Adding the bare
outentry neatly covers files alongside the existingout/directory rule.frontend/app/aipanel/aipanel.tsx (1)
532-536: Formatting-only change looks goodProps preserved; no behavior change.
pkg/aiusechat/usechat.go (1)
313-319: Pre-broadcast of tooluse events — LGTMEarly emission enables batching; guarded against nil. Ensure upstream provider handlers no longer emit initial data-tooluse to avoid duplicates.
Can you confirm that openai and anthropic paths no longer emit initial data-tooluse via SSE (only updates), so the UI won’t double-render initial entries?
frontend/app/aipanel/aimessage.tsx (1)
371-413: Thinking-state derivation looks solidNice prioritization: approvals > post-step content > reasoning. Clean integration with AIThinking.
| output: { | ||
| manualChunks(id) { | ||
| if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco"; | ||
| if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid")) | ||
| return "mermaid"; | ||
| if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex"; | ||
| if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) { | ||
| return "shiki"; | ||
| } | ||
| if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape")) | ||
| return "cytoscape"; | ||
| return undefined; | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Chunk matchers miss Monaco and scoped packages; normalize paths
- Monaco is published as monaco-editor, not monaco; current checks won’t match.
- On Windows, ids may contain backslashes.
- Mermaid is commonly @mermaid-js/; Shiki often under @shikijs/.
Refine matching and normalize separators.
Apply this diff to harden matching:
- output: {
- manualChunks(id) {
- if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
- if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
- return "mermaid";
- if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
- if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
- return "shiki";
- }
- if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
- return "cytoscape";
- return undefined;
- },
- },
+ output: {
+ manualChunks(id) {
+ const p = id.replace(/\\/g, "/");
+ if (p.includes("/node_modules/monaco-editor")) return "monaco";
+ if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/"))
+ return "mermaid";
+ if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex";
+ if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) return "shiki";
+ if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/"))
+ return "cytoscape";
+ return undefined;
+ },
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| output: { | |
| manualChunks(id) { | |
| if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco"; | |
| if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid")) | |
| return "mermaid"; | |
| if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex"; | |
| if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) { | |
| return "shiki"; | |
| } | |
| if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape")) | |
| return "cytoscape"; | |
| return undefined; | |
| }, | |
| }, | |
| output: { | |
| manualChunks(id) { | |
| const p = id.replace(/\\/g, "/"); | |
| if (p.includes("/node_modules/monaco-editor")) return "monaco"; | |
| if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/")) | |
| return "mermaid"; | |
| if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex"; | |
| if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) return "shiki"; | |
| if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/")) | |
| return "cytoscape"; | |
| return undefined; | |
| }, | |
| }, |
🤖 Prompt for AI Agents
In electron.vite.config.ts around lines 133 to 146, the manualChunks path checks
miss actual package names and Windows backslashes; normalize id by replacing
backslashes with forward slashes, and match real package names and common scoped
names: check for "monaco-editor" and both "@monaco" and "@monaco-editor" scopes,
"@mermaid-js" (and "mermaid"), "@katex" or "katex", "@shikijs" (and "shiki"),
and "@cytoscape" (and "cytoscape"); return the appropriate chunk names when
those normalized paths include the target strings. Ensure you normalize id once
at the top of manualChunks and use strict includes against the normalized path.
…vals, fix group part title
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
frontend/app/aipanel/aimessage.tsx (2)
135-158: Fix Rule of Hooks violation and use “any needs approval” for keepalive
- useEffect is declared after an early return; violates hook order.
- Keepalive is gated by group-level approval based on the first tool; others may starve. Use anyNeedApproval.
Apply:
const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { - const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null); - - if (parts.length === 0) return null; - - const firstTool = parts[0].data; - const baseApproval = userApprovalOverride || firstTool.approval; - const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; - const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); - - useEffect(() => { - if (!isStreaming || effectiveApproval !== "needs-approval") return; + const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null); + const hasParts = parts.length > 0; + const anyNeedApproval = parts.some((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); + const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); + + useEffect(() => { + if (!isStreaming || !anyNeedApproval) return; const interval = setInterval(() => { parts.forEach((part) => { RpcApi.WaveAIToolApproveCommand(TabRpcClient, { toolcallid: part.data.toolcallid, keepalive: true, }); }); }, 4000); return () => clearInterval(interval); - }, [isStreaming, effectiveApproval, parts]); + }, [isStreaming, anyNeedApproval, parts]); + + if (!hasParts) return null;Based on static analysis hints
184-191: Per-part approval for status/error and approval UI gating
- Items currently receive a group-level effectiveApproval; errors/timeouts can be misreported.
- Show Approve All only when all parts still need approval.
Apply:
- <div className="mt-1 space-y-0.5"> - {parts.map((part, idx) => ( - <AIToolUseBatchItem key={idx} part={part} effectiveApproval={effectiveApproval} /> - ))} - </div> - {allNeedApproval && effectiveApproval === "needs-approval" && ( - <AIToolApprovalButtons count={parts.length} onApprove={handleApprove} onDeny={handleDeny} /> - )} + <div className="mt-1 space-y-0.5"> + {parts.map((part, idx) => { + const partBaseApproval = userApprovalOverride || part.data.approval; + const partEffectiveApproval = + !isStreaming && partBaseApproval === "needs-approval" + ? "timeout" + : partBaseApproval; + return ( + <AIToolUseBatchItem + key={part.data.toolcallid ?? idx} + part={part} + effectiveApproval={partEffectiveApproval} + /> + ); + })} + </div> + {allNeedApproval && ( + <AIToolApprovalButtons count={parts.length} onApprove={handleApprove} onDeny={handleDeny} /> + )}
🧹 Nitpick comments (1)
frontend/app/aipanel/aimessage.tsx (1)
249-251: Optional: show explicit message when user deniesCurrently only timeouts show “Not approved.” Consider surfacing “Denied by user” for clarity.
- {(toolData.errormessage || effectiveApproval === "timeout") && ( - <div className="text-sm text-red-300 mt-1">{toolData.errormessage || "Not approved"}</div> - )} + {(toolData.errormessage || effectiveApproval === "timeout" || (userApprovalOverride || toolData.approval) === "user-denied") && ( + <div className="text-sm text-red-300 mt-1"> + {toolData.errormessage || + ((userApprovalOverride || toolData.approval) === "user-denied" ? "Denied by user" : "Not approved")} + </div> + )}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/app/aipanel/aimessage.tsx(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
WaveUIMessagePart(24-24)frontend/app/store/wshclientapi.ts (1)
RpcApi(537-537)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx
[error] 144-144: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build for TestDriver.ai
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
🔇 Additional comments (2)
frontend/app/aipanel/aimessage.tsx (2)
13-21: Thinking message fix LGTMOptional message prop + dots-only fallback is clean and solves the “AI is thinking” text issue.
383-413: Thinking-state derivation LGTMPriority for pending approvals, reasoning step, and trailing text is correct and matches UX expectations.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
electron.vite.config.ts (1)
133-147: Package name patterns still miss scoped packages and risk false positives.This issue was flagged in two previous review comments. The current substring patterns:
- Check for
monacoinstead of the actual packagemonaco-editor- Check for
@monacoinstead of the actual scope@monaco-editor- Check for
@mermaidinstead of the actual scope@mermaid-js- Check for
@shikiinstead of the actual scope@shikijsWhile substring matching may catch some cases, it's imprecise and could incorrectly chunk unrelated packages (e.g., a hypothetical
monaco-forkor@monaco-custom).Apply this diff to use precise package names and scopes:
output: { manualChunks(id) { const p = id.replace(/\\/g, "/"); - if (p.includes("node_modules/monaco") || p.includes("node_modules/@monaco")) return "monaco"; - if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid")) + if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor/")) + return "monaco"; + if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/")) return "mermaid"; - if (p.includes("node_modules/katex") || p.includes("node_modules/@katex")) return "katex"; - if (p.includes("node_modules/shiki") || p.includes("node_modules/@shiki")) { + if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex"; + if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) { return "shiki"; } - if (p.includes("node_modules/cytoscape") || p.includes("node_modules/@cytoscape")) + if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/")) return "cytoscape"; return undefined; }, },
🧹 Nitpick comments (5)
frontend/app/aipanel/aimessage.tsx (5)
83-95: Explicit button type to avoid accidental form submitAdd type="button" to both buttons (defensive best practice).
- <button + <button + type="button" onClick={onApprove} @@ - <button + <button + type="button" onClick={onDeny}
101-128: Per-item timeout/error labeling (future-proof)Batch currently passes group-wide effectiveApproval to each item. Given batches are split by approval, this is fine. To future-proof for mixed states, derive per-item effectiveApproval in the map and pass it down.
Based on learnings
-interface AIToolUseBatchItemProps { - part: WaveUIMessagePart & { type: "data-tooluse" }; - effectiveApproval: string; -} +interface AIToolUseBatchItemProps { + part: WaveUIMessagePart & { type: "data-tooluse" }; + effectiveApproval: string; // now computed per-item +}And where items are rendered (see lines 182-184):
- {parts.map((part, idx) => ( - <AIToolUseBatchItem key={idx} part={part} effectiveApproval={effectiveApproval} /> - ))} + {parts.map((part, idx) => { + const partBase = (userApprovalOverride || part.data.approval) as string; + const partEff = + !isStreaming && partBase === "needs-approval" ? "timeout" : partBase; + return ( + <AIToolUseBatchItem key={idx} part={part} effectiveApproval={partEff} /> + ); + })}
134-155: Hooks order: looks good; optional defensive guardNo early return before hooks anymore. Since AIToolUseBatch assumes non-empty parts (and AIToolUseGroup enforces it), this is safe. Optionally, add a post-effect guard to avoid parts[0] access if reused elsewhere.
Based on learnings
const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null); + const hasParts = parts.length > 0; - const firstTool = parts[0].data; + // effect first; guards on hasParts useEffect(() => { - if (!isStreaming || effectiveApproval !== "needs-approval") return; + if (!isStreaming || !hasParts) return; + // rest unchanged… }, [isStreaming, /* include */ hasParts, /* …existing deps */]); + if (!hasParts) return null; + const firstTool = parts[0].data;
354-379: Consider memoizing groupinggroupMessageParts runs each render. For long message streams, memoize on displayParts reference.
-import { memo, useEffect, useState } from "react"; +import { memo, useEffect, useMemo, useState } from "react"; @@ -const groupedParts = groupMessageParts(displayParts); +const groupedParts = useMemo(() => groupMessageParts(displayParts), [displayParts]);
434-446: Use stable keys to reduce re-mounts during streamingIndex keys can cause unnecessary remounts when parts are appended/mutated. Prefer stable keys.
-{groupedParts.map((group, index: number) => - group.type === "toolgroup" ? ( - <AIToolUseGroup key={index} parts={group.parts} isStreaming={isStreaming} /> - ) : ( - <div key={index} className="mt-2"> - <AIMessagePart part={group.part} role={message.role} isStreaming={isStreaming} /> - </div> - ) -)} +{groupedParts.map((group, index: number) => { + const key = + group.type === "toolgroup" + ? group.parts.map((p) => (p as any).data?.toolcallid || index).join(",") + : ((group.part as any).data?.toolcallid ?? `single-${index}`); + return group.type === "toolgroup" ? ( + <AIToolUseGroup key={key} parts={group.parts} isStreaming={isStreaming} /> + ) : ( + <div key={key} className="mt-2"> + <AIMessagePart part={group.part} role={message.role} isStreaming={isStreaming} /> + </div> + ); +})}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
electron.vite.config.ts(1 hunks)frontend/app/aipanel/aimessage.tsx(6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: sawka
PR: wavetermdev/waveterm#2430
File: frontend/app/aipanel/aimessage.tsx:137-144
Timestamp: 2025-10-14T06:30:54.751Z
Learning: In `frontend/app/aipanel/aimessage.tsx`, the `AIToolUseGroup` component splits file operation tool calls into separate batches (`fileOpsNeedApproval` and `fileOpsNoApproval`) based on their approval state before passing them to `AIToolUseBatch`. This ensures each batch has homogeneous approval states, making group-level approval handling valid.
📚 Learning: 2025-10-14T06:30:54.751Z
Learnt from: sawka
PR: wavetermdev/waveterm#2430
File: frontend/app/aipanel/aimessage.tsx:137-144
Timestamp: 2025-10-14T06:30:54.751Z
Learning: In `frontend/app/aipanel/aimessage.tsx`, the `AIToolUseGroup` component splits file operation tool calls into separate batches (`fileOpsNeedApproval` and `fileOpsNoApproval`) based on their approval state before passing them to `AIToolUseBatch`. This ensures each batch has homogeneous approval states, making group-level approval handling valid.
Applied to files:
frontend/app/aipanel/aimessage.tsx
🧬 Code graph analysis (1)
frontend/app/aipanel/aimessage.tsx (4)
frontend/app/aipanel/aitypes.ts (1)
WaveUIMessagePart(24-24)frontend/app/store/wshclientapi.ts (1)
RpcApi(537-537)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)pkg/aiusechat/uctypes/usechat-types.go (2)
AIMessage(263-266)AIMessagePart(268-284)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
- GitHub Check: Build for TestDriver.ai
🔇 Additional comments (5)
frontend/app/aipanel/aimessage.tsx (5)
13-21: AIThinking: correct conditional text renderingGood fix: empty string yields dots-only; null hides component.
186-189: Approve/Deny visibility condition is correctButtons shown only when allNeedApproval and still awaiting approval; matches UX intent for batch.
247-252: Single-tool “Not approved” handlingTimeout-to-“Not approved” logic is consistent with batch behavior. Keepalive effect is correctly scoped by effectiveApproval.
265-306: Good split by approval state before batchingSeparating file ops into need-approval vs no-approval ensures batch semantics are homogeneous; this validates group-level keepalive and CTA visibility.
Based on learnings
381-410: Thinking message logic reads wellPriority for pending approvals, then reasoning, then dots-only; hides when streaming text arrives. Nice.
We now show all Read File/Dir calls together and batch approve them (backend change to emit them all at once, and FE change to display them as a batch)
JS chunking for monaco, mermaid, and shiki, etc. shiki is huge, almost 10M but can't be easily split out of Streamdown. Tried making it load async, but w/ Streamdown we can't do that easily. Trying to split the JS up because of a build error we were running into in build-helper.